Skip to content

refactor: 교양 강의 조회 API 응답 성능 개선(#110) - #111

Merged
xunssoie merged 3 commits into
devfrom
analysis/110-general-education-perf
Aug 31, 2026
Merged

refactor: 교양 강의 조회 API 응답 성능 개선(#110)#111
xunssoie merged 3 commits into
devfrom
analysis/110-general-education-perf

Conversation

@xunssoie

Copy link
Copy Markdown
Member

PR Summary

교양 강의 조회 API가 요청마다 courses 테이블을 풀스캔하며 같은 결과를 다시 만드는 문제를 측정으로 진단하고, 복합 인덱스 재설계와 영역별 Redis 캐싱 두 사이클로 개선했습니다. p95 2,419 → 590 ms, 처리량 3.6배입니다 (강의 26,439건, VU 30 기준).


Problem

문제 1 - area 조건을 받칠 인덱스가 없는 풀스캔

V1_10에서 측정 근거 없이 만들어졌던 보조 인덱스를 걷어낸 뒤 교양 조회 쪽은 재설계되지 않아, 요청마다 courses 전량(26,439행)을 읽고 3컬럼 filesort까지 수행하고 있었습니다. 호출당 27,644행을 읽어 1,205행만 반환했고(읽은 행/반환 행 22.9), 이 쿼리 하나가 DB 시간의 80.3%를 차지했습니다. 그 결과 트랜잭션이 커넥션을 평균 540.5 ms 쥐어(실제 DB 실행은 157 ms) 풀 10개에 VU 30이 줄을 섰고, p95가 2,419 ms까지 밀렸습니다.


문제 2 - 인덱스 이후에도 남는 반복 재계산

인덱스로 요청당 DB 시간이 157 → 32 ms로 줄자, 매 요청이 같은 결과를 다시 만드는 비용이 최대 비용으로 올라왔습니다. 이 API는 회원과 무관하게 교양 영역 13종만으로 응답이 정해지는데도, 요청마다 강의 목록 조회와 시간표 배치 로딩(요청당 2.15회, DB 시간의 66%), 엔티티 하이드레이션(요청당 할당 15.9 MB), 574 KB 응답 조립을 반복했습니다. 커넥션 보유도 349 ms로 여전히 DB 실행(32 ms)의 10배가 넘어 풀 포화가 남아 있었습니다.


Solution

해결 1 - 측정 근거로 복합 인덱스 재설계

ALTER TABLE courses ADD INDEX idx_area_sort (area, grade_code, classification_code, haksu_code) (V1_12)

area를 선두에 둔 것은 WHERE의 실질 필터 컬럼이 area 하나라서입니다. 등호 조건이 선두에 있어야 그 뒤 컬럼들이 정렬된 상태로 남습니다. 뒤 세 컬럼은 ORDER BY(grade_code, classification_code, haksu_code)와 순서를 그대로 맞춰 정렬을 인덱스로 대신하게 했습니다. 전 행이 고유한 haksu_code까지 넣은 것은 선택도 때문이 아니라 정렬을 완성하기 위해서입니다. MySQL에는 부분 정렬 최적화가 없어 ORDER BY 컬럼이 하나라도 빠지면 결과 전체를 filesort로 다시 정렬합니다. 반대로 status는 전 행이 ACTIVE라 실제값이 1종이어서 걸러내는 행이 0이므로 뺐습니다. V1_5 시절 인덱스가 status를 선두에 뒀던 구성과 반대 방향입니다. 프리픽스는 등호를 범위 조건으로 바꿔 정렬 대체를 무너뜨려서, 커버링은 이 쿼리가 28컬럼을 전부 읽어 인덱스가 테이블 사본이 되어서 각각 배제했습니다.

실행 계획 (ACADEMIC_FOUNDATION 3,249건 기준)

항목 Before After
접근 방식 ALL (풀스캔) ref
사용 인덱스 없음 idx_area_sort
스캔 행 / 반환 행 26,439 / 3,249 3,249 / 3,249
Handler_read_rnd_next 26,440 0
Sort_rows 3,249 (병합 4회) 0 (filesort 소멸)
실행시간 40 ms 6.52 ms

해결 2 - 영역별 정적 목록 캐싱, 정원은 실시간 병합

이 경로의 데이터는 성격이 둘로 갈립니다. 강의 목록(제목, 학점, 시간표)은 관리자 동기화 때만 바뀌지만, 정원 마감 여부는 수강신청 기간에 초 단위로 변합니다. 그래서 정적 필드만 영역별로 Redis에 캐시하고, isRegisterable은 요청마다 정원 전용 쿼리(id, 현재원, 정원 - 해결 1의 인덱스를 그대로 탑니다)로 읽어 병합했습니다. 정원을 캐시에 넣는 안은 TTL을 아무리 줄여도 오답 구간이 생겨 버렸습니다.

키는 CourseArea.name() 13종입니다. 입력이 유한하고 요청 파라미터가 대문자로 정규화되어 키가 갈라지지 않습니다. 갱신은 기동 시와 매일 04:10 재적재(warm-up)가 본선이고 TTL 25시간은 재적재 실패 시의 안전망입니다. 기존 전공 캐시(04:00)와 10분 시차를 둬 재적재 DB 조회가 겹치지 않게 했습니다. 별도 무효화는 두지 않았습니다. 목록을 바꾸는 쓰기 경로가 관리자 동기화 하나뿐이고, 폐강은 정원 병합의 containsKey 필터(정원 쿼리에 status = ACTIVE 조건)가 재적재를 기다리지 않고 걸러냅니다. Redis 장애 시에는 기존 CacheErrorHandler가 DB 조회로 폴백합니다(fail-open).


부하 지표 (VU 30, 유지 1m, 교양 13영역 순환)

지표 최초 인덱스 후 캐싱 후 변화
p95 2,419.0 ms 1,709.6 ms 590.1 ms -75.6%
p99 2,783.3 ms 2,255.4 ms 806.4 ms -71.0%
처리량 (RPS) 16.71 25.76 60.52 3.6배
요청당 쿼리 수 3.15 3.15 0.99 -68.6%
쿼리 평균 실행시간 (1위 쿼리) 125.9 ms 9.9 ms 3.9 ms -96.9%
요청당 할당량 16.2 MB 15.9 MB 2.9 MB -82.1%
커넥션 보유 평균 540.5 ms 349.1 ms 147.1 ms -72.8%
GC 일시정지 (요청당) 1.341 ms 0.568 ms 0.070 ms -94.8%
캐시 적중률 - - 100% 워밍업 선적재

이번 범위에서 제외한 것

캐싱 후 최대 단일 비용은 Redis GET(요청당 1회, 평균 93.55 ms)입니다. 572 KB 값이 동시 30요청으로 Lettuce 단일 커넥션을 지나는 구조라, 직렬화 크기 축소나 로컬 캐시 계층이 다음 후보라는 사실만 기록으로 남기고 이번 이슈에서는 다루지 않았습니다. 풀 피크 포화(pending 최대 20)도 보유 시간이 147 ms로 줄어 대기 시간이 절반 이하가 된 것을 확인하고 남겨뒀습니다.


측정 조건과 산출물

강의 26,439건, 시간표 79,819건(운영 근사 대비 약 10배), VU 30(풀 10의 3배), ramp-up 30s + 유지 1m + ramp-down 30s 조건으로 쟀습니다. 로컬 측정이라 처리량과 응답시간은 하드웨어에 의존하며, 개선 판정은 하드웨어 독립 증거(읽은 행/반환 행, Handler 카운터, 요청당 쿼리 수와 할당량, 캐시 적중률)로 했습니다. 측정 기록과 실행계획 원본은 .claude/resources/perf/110/general-education/record.md에 있습니다. 운영 반영 시 prod 프로파일은 현재 spring.cache.type: none이라 캐시가 비활성이므로 Redis 설정과 재적재 크론 추가가 필요하고, V1_12 인덱스 추가의 운영 소요 시간은 운영 행 수를 몰라 단정하지 않았습니다.


Related Issue

@xunssoie xunssoie self-assigned this Aug 31, 2026
@github-actions

Copy link
Copy Markdown

Test Results

290 tests   290 ✅  4s ⏱️
 93 suites    0 💤
 93 files      0 ❌

Results for commit b999c43.

@xunssoie
xunssoie merged commit 204fad4 into dev Aug 31, 2026
2 checks passed
@xunssoie xunssoie added the Refactor 기존 레거시 코드를 리팩토링합니다. label Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Refactor 기존 레거시 코드를 리팩토링합니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

analysis: 교양 강의 조회 API 성능 측정 및 병목 진단

1 participant